package de.unikoblenz.west.csxpoi.server;

import java.util.ArrayList;
import java.util.List;

//import org.openrdf.query.Binding;
import org.openrdf.query.BindingSet;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.TupleQueryResult;
import org.openrdf.repository.RepositoryConnection;
import org.openrdf.repository.RepositoryException;

/**
 * Encapsulates POIs, which are identified by their URI and have a name, a
 * latitude, a longitude, an altitude, categories, and equivalents.
 */
public class PoiWrapper {

	/**
	 * the URI of the POI
	 */
	private String mUri = "";

	/**
	 * the name of the POI
	 */
	private String mName = "";

	/**
	 * the type of the POI
	 */
	private String mType = "";

	/**
	 * the latitude of the POI
	 */
	private double mLatitude = 0;

	/**
	 * the longitude of the POI
	 */
	private double mLongitude = 0;

	/**
	 * the altitude of the POI
	 */
	private double mAltitude = 0;

	/**
	 * the list of categories of the POI
	 */
	private List<CategoryWrapper> mCategories = new ArrayList<CategoryWrapper>();

	/**
	 * the list of equivalents of the POI
	 */
	private List<PoiWrapper> mEquivalents = new ArrayList<PoiWrapper>();

	/**
	 * whether the POI is obsolete
	 */
	private boolean mObsolete = false;

	/**
	 * the cluster the POI belongs to
	 */
	private Cluster mCluster = Cluster.NONE;

	// TODO
	private String mStartDate = "";
	private String mEndDate = "";

	/**
	 * Creates a POI from the specified URI by retrieving all properties from
	 * the repository.
	 * 
	 * @param uri
	 *            the URI of the POI
	 * @param connection
	 *            a repository connection
	 * @return a POI with the given URI and the retrieved properties or null if
	 *         the creation failed
	 * @throws RepositoryException
	 * @throws MalformedQueryException
	 * @throws QueryEvaluationException
	 * @throws NumberFormatException
	 */
	public static PoiWrapper createFromUri(String uri,
			RepositoryConnection connection) throws RepositoryException,
			MalformedQueryException, QueryEvaluationException,
			NumberFormatException {

		PoiWrapper poi = null;

		// Retrieve name, latitude, longitude, altitude, and optional event
		// start and end:
		TupleQuery query = connection.prepareTupleQuery(QueryLanguage.SPARQL,
				"PREFIX base: <" + Constants.NS_BASE + "> " + " PREFIX geo: <"
						+ Constants.NS_GEO + "> " + " PREFIX rdfs: <"
						+ Constants.NS_RDFS + "> " + " PREFIX gle: <"
						+ Constants.NS_GLE + "> "
						+ " SELECT ?name ?lat ?long ?alt ?start ?end "
						+ " WHERE {" + " ?poi a base:Poi ;"
						+ " rdfs:label ?name ;" + " geo:lat ?lat ;"
						+ " geo:long ?long ;" + " geo:alt ?alt ."
						+ " OPTIONAL { ?poi gle:hasStartDateTime ?start . } "
						+ " OPTIONAL { ?poi gle:hasEndDateTime ?end . } "
						+ " OPTIONAL { ?removal a base:PoiRemoval ;"
						+ " base:affectsPoi ?poi . }"
						+ " FILTER (!bound(?removal))" + " FILTER (?poi = <"
						+ uri + ">) } " + " LIMIT 1");

		TupleQueryResult result = query.evaluate();
		try {
			if (result.hasNext()) {
				BindingSet bindingSet = result.next();
				poi = createFromBinding(bindingSet, uri, connection);
			}
		} finally {
			result.close();
		}
		return poi;
	}

	public static PoiWrapper createFromBinding(BindingSet bindingSet,
			String uri, RepositoryConnection connection) {

		PoiWrapper poi = new PoiWrapper();
		poi.setUri(uri);
		poi.setName(bindingSet.getValue("name").stringValue());
		poi.setLatitude(Double
				.valueOf(bindingSet.getValue("lat").stringValue()));
		poi.setLongitude(Double.valueOf(bindingSet.getValue("long")
				.stringValue()));
		poi.setAltitude(Double
				.valueOf(bindingSet.getValue("alt").stringValue()));
		if (bindingSet.hasBinding("start"))
			poi.setStartDate(bindingSet.getValue("start").stringValue());
		if (bindingSet.hasBinding("end"))
			poi.setEndDate(bindingSet.getValue("end").stringValue());

		return retrieveCategories(poi, connection, uri);
	}

	private static PoiWrapper retrieveCategories(PoiWrapper poi,
			RepositoryConnection connection, String uri) {
		try {
			if (poi != null) {
				// Retrieve categories:
				TupleQuery categoriesQuery = connection.prepareTupleQuery(
						QueryLanguage.SPARQL, "PREFIX base: <"
								+ Constants.NS_BASE + "> " + "PREFIX rdfs: <"
								+ Constants.NS_RDFS + "> "
								+ "SELECT ?category ?name WHERE {" + " <" + uri
								+ "> a ?category; rdfs:label ?name. "
								+ " ?category rdfs:subClassOf base:Poi ."								
								+ " OPTIONAL {"
									+ "  ?removal a base:CategoryRemoval ;"
									+ "   base:affectsCategory ?category ." + " }"
									+ " FILTER (!bound(?removal) && " 
									+ " ?category != base:Poi)" + "}");
				
				
		
				
				TupleQueryResult categoriesResult = categoriesQuery.evaluate();
				try {
					while (categoriesResult.hasNext()) {
						BindingSet bindingSet = categoriesResult.next();
						CategoryWrapper category = CategoryWrapper
								.createFromUri(bindingSet.getValue("category")
										.stringValue(), connection);
						if (category != null) {
							poi.addCategory(category);
							List<CategoryWrapper> superClasses = category
									.getSuperclasses();
							if (superClasses != null && superClasses.size() > 0)
								for (CategoryWrapper superclass : superClasses) {
									poi.addCategory(superclass);
								}
						}
					}
				} finally {
					categoriesResult.close();
				}

				// Retrieve modifications:
				TupleQuery modificationsQuery = connection
						.prepareTupleQuery(
								QueryLanguage.SPARQL,
								"PREFIX base: <"
										+ Constants.NS_BASE
										+ "> "
										+ "PREFIX rdfs: <"
										+ Constants.NS_RDFS
										+ "> "
										+ "SELECT ?poi ?changes ?adds ?deletes ?value WHERE {"
										+ " ?modification a base:PoiModification ;"
										+ "  base:affectsPoi ?poi ;"
										+ "  base:issuedAt ?issuedAt ."
										+ " OPTIONAL { ?modification base:changesPoiProperty ?changes }"
										+ " OPTIONAL { ?modification base:addsPoiProperty ?adds }"
										+ " OPTIONAL { ?modification base:deletesPoiProperty ?deletes }"
										+ " OPTIONAL { ?modification base:poiPropertyValue ?value }"
										+ " FILTER (?poi = <" + uri + ">"
										+ "  || (bound(?value) && ?value = <"
										+ uri + ">))"
										+ "} ORDER BY ASC(?issuedAt)");
				// TODO: consider category modifications of all equivalents
				TupleQueryResult modificationsResult = modificationsQuery
						.evaluate();
				try {
					// A POI is obsolete when it points to an equivalent:
					int obsolete = 0;
					while (modificationsResult.hasNext()) {
						BindingSet bindingSet = modificationsResult.next();
						if (bindingSet.hasBinding("changes")) {
							poi.set(bindingSet.getValue("changes")
									.stringValue(), bindingSet
									.getValue("value").stringValue());
						} else if (bindingSet.hasBinding("adds")) {
							if (bindingSet.getValue("adds").stringValue()
									.equals(Constants.URI_RDF_TYPE)) {
								CategoryWrapper category = CategoryWrapper
										.createFromUri(
												bindingSet.getValue("value")
														.stringValue(),
												connection);
								if (category != null) {
									poi.addCategory(category);
									for (CategoryWrapper superclass : category
											.getSuperclasses()) {
										poi.addCategory(superclass);
									}
								}
							} else if (bindingSet.getValue("adds")
									.stringValue()
									.equals(Constants.URI_OWL_SAME_AS)) {
								if (bindingSet.getValue("poi").stringValue()
										.equals(uri)) {
									PoiWrapper equivalent = PoiWrapper
											.createFromUri(
													bindingSet
															.getValue("value")
															.stringValue(),
													connection);
									if (equivalent != null)
										poi.addEquivalent(equivalent);
									obsolete++;
								} else {
									PoiWrapper equivalent = new PoiWrapper();
									equivalent.setUri(bindingSet
											.getValue("poi").stringValue());
									poi.addEquivalent(equivalent);
								}
							}
						} else if (bindingSet.hasBinding("deletes")) {
							if (bindingSet.getValue("deletes").stringValue()
									.equals(Constants.URI_RDF_TYPE)) {
								poi.deleteCategory(bindingSet.getValue("value")
										.stringValue());
							} else if (bindingSet.getValue("deletes")
									.stringValue()
									.equals(Constants.URI_OWL_SAME_AS)) {
								if (bindingSet.getValue("poi").stringValue()
										.equals(uri)) {
									poi.deleteEquivalent(bindingSet.getValue(
											"value").stringValue());
									obsolete--;
								} else {
									poi.deleteEquivalent(bindingSet.getValue(
											"poi").stringValue());
								}
							}
						}
					}
					if (obsolete > 0) {
						poi.setObsolete(true);
					}
				} finally {
					modificationsResult.close();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return poi;
	}

	/**
	 * Tests whether the POI matches the specified query, i.e. whether the query
	 * is a substring of the POI's name or of one of the POI's categories'
	 * names.
	 * 
	 * @param query
	 *            the query
	 * @return true if the POI matches the query
	 */
	public boolean matchesQuery(String query) {
		if (query.equals(""))
			return true;
		String normalizedQuery = query.toLowerCase().trim();
		if (mName.toLowerCase().contains(normalizedQuery))
			return true;
		for (CategoryWrapper category : mCategories) {
			if (category.getName().toLowerCase().contains(normalizedQuery))
				return true;
		}
		return false;
	}

	/**
	 * Sets the URI of the POI.
	 * 
	 * @param uri
	 *            the URI to set
	 */
	public void setUri(String uri) {
		mUri = uri;
	}

	/**
	 * Sets the name of the POI.
	 * 
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		mName = name;
	}

	/**
	 * Sets the latitude of the POI.
	 * 
	 * @param lat
	 *            the latitude to set
	 */
	public void setLatitude(double lat) {
		mLatitude = lat;
	}

	/**
	 * Sets the longitude of the POI.
	 * 
	 * @param lng
	 *            the longitude to set
	 */
	public void setLongitude(double lng) {
		mLongitude = lng;
	}

	/**
	 * Sets the altitude of the POI.
	 * 
	 * @param alt
	 *            the altitude to set
	 */
	public void setAltitude(double alt) {
		mAltitude = alt;
	}

	/**
	 * Adds a category to the POI. It is added to the list of categories unless
	 * it is already present.
	 * 
	 * @param category
	 *            the category to add
	 */
	public void addCategory(CategoryWrapper category) {
		if (!mCategories.contains(category))
			mCategories.add(category);
	}

	/**
	 * Adds a category to the POI, which is created from its URI. If the
	 * creation fails, no category is added.
	 * 
	 * @param uri
	 *            the URI of the category to add
	 * @param connection
	 *            a repository connection
	 * @throws RepositoryException
	 * @throws MalformedQueryException
	 * @throws QueryEvaluationException
	 */
	public void addCategory(String uri, RepositoryConnection connection)
			throws RepositoryException, MalformedQueryException,
			QueryEvaluationException {

		CategoryWrapper category = CategoryWrapper.createFromUri(uri,
				connection);
		if (category != null)
			addCategory(category);

	}

	/**
	 * Deletes a category of the POI. It is removed from the list of categories.
	 * 
	 * @param uri
	 *            the URI of the category to delete
	 */
	public void deleteCategory(String uri) {
		mCategories.remove(new CategoryWrapper(uri, null));
	}

	/**
	 * Adds an equivalent to the POI. It is added to the list of equivalents
	 * unless it is already present.
	 * 
	 * @param equivalent
	 *            the equivalent to add
	 */
	public void addEquivalent(PoiWrapper equivalent) {
		if (!mEquivalents.contains(equivalent))
			mEquivalents.add(equivalent);
	}

	/**
	 * Adds an equivalent to the POI, which is created from its URI. If the
	 * creation fails, no equivalent is added.
	 * 
	 * @param uri
	 *            the URI of the equivalent to add
	 * @param connection
	 *            a repository connection
	 * @throws RepositoryException
	 * @throws MalformedQueryException
	 * @throws QueryEvaluationException
	 */
	public void addEquivalent(String uri, RepositoryConnection connection)
			throws RepositoryException, MalformedQueryException,
			QueryEvaluationException {
		PoiWrapper equivalent = PoiWrapper.createFromUri(uri, connection);
		if (equivalent != null)
			addEquivalent(equivalent);
	}

	/**
	 * Deletes an equivalent of the POI. It is removed from the list of
	 * equivalents.
	 * 
	 * @param uri
	 *            the URI of the equivalent to delete
	 */
	public void deleteEquivalent(String uri) {
		PoiWrapper poi = new PoiWrapper();
		poi.setUri(uri);
		mEquivalents.remove(poi);
	}

	/**
	 * Sets a property of the POI identified by its URI.
	 * 
	 * @param propertyUri
	 *            the URI of the property
	 * @param value
	 *            the value to set for the specified property
	 * @throws NumberFormatException
	 */
	public void set(String propertyUri, String value)
			throws NumberFormatException {
		if (propertyUri.equals(Constants.URI_RDFS_LABEL)) {
			setName(value);
		} else if (propertyUri.equals(Constants.URI_GEO_LAT)) {
			setLatitude(Double.valueOf(value));
		} else if (propertyUri.equals(Constants.URI_GEO_LONG)) {
			setLongitude(Double.valueOf(value));
		} else if (propertyUri.equals(Constants.URI_GEO_ALT)) {
			setAltitude(Double.valueOf(value));
		} else if (propertyUri.equals(Constants.URI_HAS_START_DATETIME)) {
			setStartDate(value);
		} else if (propertyUri.equals(Constants.URI_HAS_END_DATETIME)) {
			setEndDate(value);
		}
	}

	/**
	 * Set whether the POI is obsolete.
	 * 
	 * @param obsolete
	 *            whether the POI is obsolete
	 */
	public void setObsolete(boolean obsolete) {
		mObsolete = obsolete;
	}

	/**
	 * Gets the URI of the POI.
	 * 
	 * @return the URI of the POI
	 */
	public String getUri() {
		return mUri;
	}

	/**
	 * Gets the name of the POI.
	 * 
	 * @return the name of the POI
	 */
	public String getName() {
		return mName;
	}

	public synchronized String getType() {
		return mType;
	}

	public synchronized void setType(String type) {
		this.mType = type;
	}

	/**
	 * Gets the latitude of the POI.
	 * 
	 * @return the latitude of the POI
	 */
	public double getLatitude() {
		return mLatitude;
	}

	/**
	 * Gets the longitude of the POI.
	 * 
	 * @return the longitude of the POI
	 */
	public double getLongitude() {
		return mLongitude;
	}

	/**
	 * Gets the altitude of the POI.
	 * 
	 * @return the altitude of the POI
	 */
	public double getAltitude() {
		return mAltitude;
	}

	/**
	 * Gets the categories of the POI.
	 * 
	 * @return a copy of the list of categories of the POI
	 */
	public List<CategoryWrapper> getCategories() {
		return new ArrayList<CategoryWrapper>(mCategories);
	}

	/**
	 * Gets the equivalents of the POI.
	 * 
	 * @return a copy of the list of equivalents of the POI
	 */
	public List<PoiWrapper> getEquivalents() {
		return new ArrayList<PoiWrapper>(mEquivalents);
	}

	/**
	 * Tests whether the POI is obsolete.
	 * 
	 * @return whether the POI is obsolete
	 */
	public boolean isObsolete() {
		return mObsolete;
	}

	/**
	 * POIs are equal if their URI match.
	 */
	@Override
	public boolean equals(Object object) {
		try {
			PoiWrapper poi = (PoiWrapper) object;
			return poi.getUri().equals(mUri);
		} catch (ClassCastException e) {
			return false;
		} catch (NullPointerException e) {
			return false;
		}
	}

	/**
	 * Gets the cluster the POI belongs to.
	 * 
	 * @return the cluster the POI belongs to
	 */
	public Cluster getCluster() {
		return mCluster;
	}

	/**
	 * Adds the POI to the specified cluster after removing it from its previous
	 * cluster.
	 * 
	 * @param cluster
	 *            the cluster to add the POI to
	 */
	public void addToCluster(Cluster cluster) {
		mCluster.removeMember(this);
		cluster.addMember(this);
		mCluster = cluster;
	}

	public String getStartDate() {
		return mStartDate;
	}

	public void setStartDate(String startDate) {
		mStartDate = startDate;
	}

	public String getEndDate() {
		return mEndDate;
	}

	public void setEndDate(String endDate) {
		mEndDate = endDate;
	}
}
